#pragma once

#include <iostream>
#include <string>
#include <fstream>
#include <vector>
#include <algorithm>
#include <mutex>
#include <cassert>
#include <jsoncpp/json/json.h>

#include "../comm/util.hpp"
#include "../comm/log.hpp"
#include "../comm/httplib.h"
#include "oj_model.hpp"
// #include "oj_model2.hpp"
#include "oj_view.hpp"

namespace ns_control
{
    using namespace std;
    using namespace ns_log;
    using namespace ns_util;
    using namespace ns_model;
    using namespace ns_view;
    using namespace httplib;

    // 提供服务的主机 
    class Machine
    {
    public:
        std::string ip;  //编译服务的ip
        int port;        //编译服务的port
        uint64_t load;   //编译服务的负载
        std::mutex *mtx; // mutex是禁止拷贝的，因此我们只能使用指针
        // 为什么成员变量里面要有一把锁？
            // 为了避免多线程并发访问所带来的数据不一致问题
            // 考虑下面的情况，该Machine中的load=10，如果有两个线程A和B，都要对该Machine中的load进行加一的操作
            // 正常来说，这两个线程执行完毕之后，该Machine的load值应该为12
            // 但是不加锁的情况下，如果按照下面的执行顺序，就会导致最终的load值不是12
                // 线程 A 读取load值为 10。
                // 线程 B 同时读取load值也为 10。
                // A 将值加 1 后写入 11，B 同样加 1 后也写入 11，最终load结果为 11（本应是 12）。
    public:
        Machine() : ip(""), port(0), load(0), mtx(nullptr)
        {
        }
        ~Machine()
        {
        }

    public:
        // 为了避免出现多线程并发执行带来的数据不一致问题，我们修改load值的操作需要加锁
        // 提升主机负载
        void IncLoad()
        {
            if (mtx) mtx->lock();
            ++load;
            if (mtx) mtx->unlock();
        }
        // 减少主机负载
        void DecLoad()
        {
            if (mtx) mtx->lock();
            --load;
            if (mtx) mtx->unlock();
        }
        // 将该主机的负载重置为0
        void ResetLoad()
        {
            if(mtx) mtx->lock();
            load = 0;
            if(mtx) mtx->unlock();
        }
        // 获取主机负载,没有太大的意义，只是为了统一接口
        uint64_t Load()
        {
            uint64_t _load = 0;
            if (mtx) mtx->lock();
            _load = load;
            if (mtx) mtx->unlock();

            return _load;
        }
    };

    const std::string service_machine = "./conf/service_machine.conf";
    // 负载均衡模块
    class LoadBlance
    {
    private:
        // 可以给我们提供编译服务的所有的主机
        // 每一台主机都有自己的下标，充当当前主机的id
        std::vector<Machine> machines;
        // 所有在线的主机id
        std::vector<int> online;
        // 所有离线的主机id
        std::vector<int> offline;
        // 保证LoadBlance它的数据安全
        std::mutex mtx;

    public:
        LoadBlance()
        {
            assert(LoadConf(service_machine));
            // 走到这里说明参与负载均衡的主机信息已经被成功提取到 machines数组中
            LOG(INFO) << "加载 " << service_machine << " 成功"
                      << "\n";
        }
        ~LoadBlance()
        {
        }

    public:
        // 读取machine.conf文件，将其中的主机信息按行为单位提取出来，描述成一个个的Machine结构体
        // 最后存放入成员变量machines数组中，如果信息提取成功，则返回true
        bool LoadConf(const std::string &machine_conf)
        {
            // machine_conf里面记录的是负载均衡主机配置文件的完整路径，即"./conf/service_machine.conf";
            // 下面我们首先要读取这个文件，把文件里面的主机信息都读到我们内存的数据结构中
            std::ifstream in(machine_conf);
            if (!in.is_open())
            {
                LOG(FATAL) << " 加载: " << machine_conf << " 失败"
                           << "\n";
                return false;
            }
            std::string line;
            while (std::getline(in, line))
            {  
                // 走到这里，getline已经成功从文件中又读了一行，并将读到的内容存入string类型的line中
                std::vector<std::string> tokens;// 这个数组用来存储被分割后的一个个子字符串
                // SplitString这个函数对line这个字符串以":"为分隔符进行分割，并将分割后的结果存储在tokens数组中
                StringUtil::SplitString(line, &tokens, ":");
                if (tokens.size() != 2)
                {
                    // 因为service_machine.conf文件中，一行数据的格式如下：127.0.0.1:8083
                    // 所以按照正常情况来说，就可以得到两个子字符串，所以如果子字符串的数量不是2，就说明分割失败了
                    LOG(WARNING) << " 切分 " << line << " 失败"
                                 << "\n";
                    continue;
                }
                // 走到这里说明line中的ip和端口号已经被读到了tokens[0]和tokens[1]中
                // 现在我们就进一步将当前主机信息封装在结构体中
                Machine m;
                m.ip = tokens[0];
                m.port = atoi(tokens[1].c_str());
                m.load = 0;
                m.mtx = new std::mutex();
                // 现在我们已经成功把conf文件中当前行的信息提取到了Machine类的对象m中

                // online数组中存放的是在线主机的编号，所谓编号指的是主机在machines数组中的下标
                // 当前主机马上要尾插到machines数组中，其下标恰好就是没插之前的machines.size()
                online.push_back(machines.size());

                // 将对象m插入到数组machines中
                machines.push_back(m);
            }

            // 到此为止，我们已经把.conf文件中每一行的信息都描述成Machine对象，存入了machines数组中
            in.close();
            return true;
        }
        
        // 负载均衡：当客户端来请求的时候，我们要从这几台主机里选一台最不忙的主机，让他来处理这个请求
        bool SmartChoice(int *id, Machine **m)
        {
            // id: 输出型参数
            // m : 输出型参数   
            // 1. 使用选择好的主机(更新该主机的负载)
            // 2. 我们需要可能离线该主机
            
            mtx.lock();
            // 下面我们就要从online数组中，选一台主机出来，让它来处理这个请求
            // 具体应该选谁呢？（负载均衡的算法）
            // 1. 随机数+hash
            // 2. 轮询+hash
            int online_num = online.size();
            if (online_num == 0)// 如果online数组为空，说明没有一台主机是在线的，无法分配
            {
                mtx.unlock();
                LOG(FATAL) << " 所有的后端编译主机已经离线, 请运维的同事尽快查看"
                           << "\n";
                return false;
            }
            // 走到这里说明online数组不为空，因此我们就要遍历online数组中的主机，寻找当前时刻负载最小的那个主机
            // 通过遍历的方式，找到所有负 载最小的机器
            *id = online[0]; // id是一个输出型参数，等会我们找到那个负载最小的在线主机之后，*id就等于该主机的id
            *m = &machines[online[0]]; // 二级指针m也是一个输出型参数，等会我们找到那个负载最小的在线主机之后，*m就等于该主机Machine对象的地址
            uint64_t min_load = machines[online[0]].Load();// min_load最终记录machines数组中最小主机负载
            // 遍历machines数组，寻找负载最小的主机
            for (int i = 1; i < online_num; i++)
            {
                uint64_t curr_load = machines[online[i]].Load(); // 当前遍历的主机负载
                if (min_load > curr_load) //  如果当前遍历的主机负载是目前最小的，那就让前面几个参数都改成当前主机的配置
                {
                    min_load = curr_load;
                    *id = online[i];
                    *m = &machines[online[i]];
                }
            }
            mtx.unlock();
            return true;
        }
        void OfflineMachine(int which)
        {
            mtx.lock();
            for(auto iter = online.begin(); iter != online.end(); iter++)
            {
                if(*iter == which)
                {
                    // 因为编号为which的主机已经离线了，不管以前其负载是多少，现在都得置0
                    machines[which].ResetLoad();
                    //然后就是将其从online中删掉，并添加到offline中
                    online.erase(iter);
                    offline.push_back(which);
                    break; //因为break的存在，所有我们暂时不考虑迭代器失效的问题
                }
            }
            mtx.unlock();
        }
 
        // 将所有离线的主机统一上线
        void OnlineMachine()
        {

            mtx.lock();
            // 将所有离线的主机都插入到online队列中
            online.insert(online.end(), offline.begin(), offline.end());
            // 删除offline队列中的所有元素
            offline.erase(offline.begin(), offline.end());
            mtx.unlock();

            LOG(INFO) << "所有的主机都上线啦!" << "\n";
        } 

        // 打印出当前在线的主机列表和离线的主机列表
        void ShowMachines()
        {
             mtx.lock();
             std::cout << "当前在线主机列表: ";
             for(auto &id : online)
             {
                 std::cout << id << " ";
             }
             std::cout << std::endl;
             std::cout << "当前离线主机列表: ";
             for(auto &id : offline)
             {
                 std::cout << id << " ";
             }
             std::cout << std::endl;
             mtx.unlock();
        }
    };

    // 这是我们的核心业务逻辑的控制器
    class Control
    {
    private:
        Model model_; //提供后台数据
        View view_;   //提供html渲染功能
        LoadBlance load_blance_; //核心负载均衡器
    public:
        Control()
        {
        }
        ~Control()
        {
        }

    public:
        void RecoveryMachine()
        {
            load_blance_.OnlineMachine();
        }

        //根据题目数据构建网页
        // html: 输出型参数
        bool AllQuestions(string *html)
        {
            bool ret = true; // 用来表示网页构建是否成功
            vector<struct Question> all;
            if (model_.GetAllQuestions(&all))
            {
                // 走到这里说明Question内部的哈希表不为空，GetAllQuestions已经成功地将哈希表中的question都读入了all中

                // 下面我们就要对数组中的这些question按照问题序号进行排序，即将序号为1的问题放在all[0]的位置，序号为2的问题放在all[1]的位置，依次类推
                sort(all.begin(), all.end(), [](const struct Question &q1, const struct Question &q2){
                    return atoi(q1.number.c_str()) < atoi(q2.number.c_str());
                });
                // 获取题目信息成功，将所有的题目数据构建成网页
                view_.AllExpandHtml(all, html);
            }
            else
            {
                // 走到这里说明GetAllQuestions返回的是false，说明并没有获取到题目列表
                *html = "获取题目失败, 形成题目列表失败";
                ret = false;
            }
            return ret;
        }
        bool Question(const string &number, string *html)
        {
            bool ret = true;
            struct Question q;

            // GetOneQuestion()作用：根据题号number，在哈希表中查找题目，找到之后将struct Question类型的题目存入输出型参数Question *q中
            if (model_.GetOneQuestion(number, &q))
            {
                // 走到这里说明我们已经成功地将题目读到了struct Question类型的对象q中
                // 下面我们就根据该题目的信息，对网页进行渲染，将渲染完成之后的html语句存到指针html指向的string中
                view_.OneExpandHtml(q, html);
            }
            else
            {
                *html = "指定题目: " + number + " 不存在!";
                ret = false;
            }
            return ret;
        }

        // code: #include...
        // input: ""
        // Judge的工作就是，将用户提交的代码，与我们tail.cpp文件进行合并，合并成一个完整的测试代码，然后对代码进行编译和运行，将运行的结果写入输出型参数out_json中
        void Judge(const std::string &number, const std::string in_json, std::string *out_json)
        {
            // 这里传入的in_json是要发送给服务器的http请求报文的正文部分，out_json是输出型参数，用来接收用户代码执行之后的最终结果
            // LOG(DEBUG) << in_json << " \nnumber:" << number << "\n";
            
            // 0. 根据题目编号，直接拿到对应的题目细节
            struct Question q;
            model_.GetOneQuestion(number, &q);

            // 1. 对in_json进行反序列化，将json字符串中的信息提取到Json::Value类型的结构体中，获得用户刷的那题的题号，同时也获得用户提交的源代码
            Json::Reader reader;
            Json::Value in_value;
            reader.parse(in_json, in_value);// 对in_json字符串进行反序列化，将反序列化后得到的结果存储到Json::Value类的对象in_value中

            // 用户提交的代码就存储在in_value["code"]中
            std::string code = in_value["code"].asString();
            // in_value["code"]返回的类型是 Json::Value，但是实际上它是一个字符串，于是我们就调用asString()，将其强转成一个字符串

            // 2. 重新拼接用户代码+测试用例代码，形成新的代码
            Json::Value compile_value;
            compile_value["input"] = in_value["input"].asString();
            compile_value["code"] = code + "\n" + q.tail;
            compile_value["cpu_limit"] = q.cpu_limit;
            compile_value["mem_limit"] = q.mem_limit;
            // 到此为止，我们就已经将两端代码拼成了一段代码

            // 将整合完毕之后的代码再重新序列化，形成一份合并之后的json字符串compile_string
            Json::FastWriter writer;
            std::string compile_string = writer.write(compile_value);
                // writer.write()的作用是将Json::Value类型的compile_value序列化为 JSON 格式的字符串
            LOG(INFO) << "拼接用户代码+测试用例代码，形成完整的测试代码" << compile_string<<"\n";
            // 3. 选择负载最低的主机(差错处理)
            // 规则: 一直选择，直到主机可用，否则，就是全部挂掉
            while(true)
            {
                int id = 0;
                Machine *m = nullptr;
                if(!load_blance_.SmartChoice(&id, &m)) 
                {
                    // 走到这里说明所有主机都下线了，没有在线的可供分配的主机
                    // 我们直接退出
                    break;
                }

                // 走到这里说明有在线可供分配的主机，并且我们在SmartChoice函数中已经完成了主机的挑选，挑选出来的主机信息存储在id和m中
                // id中记录的是负载最低的主机编号，m中记录的是负载最低的主机Machine结构体地址

                // oj_server负责总览大局，其并不负责具体程序的编译运行工作，在前面通过负载均衡策略，已经选出了那个负责具体编译运行程序的主机，假设是主机A
                // 现在我oj_server向主机A中的compile_server进程发起http请求，得到结果
                Client cli(m->ip, m->port);
                m->IncLoad();// 连上之后，主机A中的compile_server进程就多了一个编译运行的任务，负载加一
                LOG(INFO) << " 选择主机成功, 主机id: " << id << " 详情: " << m->ip << ":" << m->port << " 当前主机的负载是: " << m->Load() << "\n";
                
                if(auto res = cli.Post("/compile_and_run", compile_string, "application/json;charset=utf-8"))
                {   // compile_string是我前面把用户代码和测试代码合并之后，再序列化形成的json字符串
                    // cli.Post("/compile_and_run", compile_string, "application/json;charset=utf-8")的作用就是：
                        // 构建一个POST请求报文，其正文部分就是 compile_string，向服务器请求的资源路径就是"/compile_and_run"
                        // 请求报头的Content-Type字段值是 application/json;charset=utf-8，意思就是告诉接受窗口，其接收正文的格式为 JSON，字符编码为 UTF-8
                    // 由于我们在compile_server.cc中也定义了请求资源路径为"/compile_and_run"时POST的处理函数，其功能是编译并运行compile_string中的代码，完了之后将运行结果以回送报文的形式发回来
                    // cli.POST的返回值res，就是用来接收服务器发来的http回送报文的
                    // 如果服务器发送的回送报文抵达了客户端，其会被转化成Response类型的对象，存储到客户端的主机中，res里面存的就是这个response对象的指针

                    // 5. 将结果赋值给out_json
                    LOG(INFO)<< "回送报文状态码："<<res->status<<endl;
                    if(res->status == 200) // 状态码是200说明正常
                    {  
                        cout << res->body<<endl;
                        *out_json = res->body; // 将回送报文的正文部分写入输出型参数out_json中
                        m->DecLoad();// 请求处理完毕，compile_server负载减一
                        LOG(INFO) << "请求编译和运行服务成功..." << "\n";
                        break;
                        // 这个break退出的循环是最外面的那个while(true)循环
                        // 也就是说走到这里，就表示本次Judge函数执行完毕了
                    }
                    m->DecLoad();// 请求处理完毕，compile_server负载减一
                }
                else
                {
                    // 在 cpp-httplib 库中，Client::Post 方法的返回值类型是 Result，它是一个智能指针（std::shared_ptr<Response>）
                    // 指向一个包含 HTTP 响应信息的对象。如果请求失败，返回的指针将为空（nullptr）
                    // 走到这里说明res==nullptr，这就说明Post返回值为空，即客户端POST请求失败，客户端并没有成功连上目的主机
                    // 此时我们就认为，此时客户端要连的那台主机离线了
                    LOG(ERROR) << " 当前请求的主机id: " << id << " 详情: " << m->ip << ":" << m->port << " 可能已经离线"<< "\n";
                    // 下面我们要将客户端请求的那台目的主机设置为离线状态
                    load_blance_.OfflineMachine(id); // 将当前主机的id从online队列中移除，并加入到offline中
                    load_blance_.ShowMachines(); //仅仅是为了用来调试
                }
            }
        }
    };
} // namespace name
